/**
 * Cross-browser Dom extensions for HTMLElement objects
 * 
 * @author  Mykhailo Stadnyk
 * @package Jsl
 * @version 1.0
 */
(function() {

var
	w      = window,
	d      = w.document,
	ce     = d.createElement,
	gi     = d.getElementById,
	gt     = d.getElementsByTagName,
	gn     = d.getElementsByName || false,
	gc     = d.getElementsByClassName || false,
	C      = Jsl.Collection,
	Sizzle = w.Sizzle
;

/**
 * IE does not support prototyping of HTMLElement,
 * so we will emulate this via overriding document
 * methods
 */
Jsl.isIe && !Jsl.isIe9 && !Jsl.isIe10 && (HTMLElement = function() {

	ce && (d.createElement = function( tag) {
		return Jsl.applyIf( ce( tag), HTMLElement.prototype);
	});

	gi && (d.getElementById = function( id) {
		return Jsl.applyIf( gi( id), HTMLElement.prototype);
	});

	gt && (d.getElementsByTagName = function( tag) {
		var els = gt( tag);
		Jsl.each( els, function( el) {
			Jsl.applyIf( el, HTMLElement.prototype);
		});
		return els;
	});

	gn && (d.getElementsByName = function( name) {
		var els = gn( name);
		Jsl.each( els, function( el) {
			Jsl.applyIf( el, HTMLElement.prototype);
		});
		return els;
	});

	gc && (d.getElementsByClassName = function( name) {
		var els = gc( name);
		Jsl.each( els, function( el) {
			Jsl.applyIf( el, HTMLElement.prototype);
		});
		return els;
	});

	return function() {};

}());

/**
 * Extending HTML DOM elements
 */
Jsl.applyIf( HTMLElement.prototype, function() {

	/**
	 * FAKE ARGS:
	 *   new       - populate new collection form return values
	 *   undefined - populate self
	 *   return    - make single call, populate returned value
	 */
	C.$populate = function() {};
	var _removeElement = function() {
		C.$populate();
		var	self = this;
		
		Jsl.freeElement( self);
		res = this.parentNode.removeChild( self);
		self = null;
		
		try {
			return res
		} finally {
			res = null
		}
	};
	
	if (Element && Element.prototype.remove) {
		Element.prototype.remove = _removeElement;
	}

	return {

	/**
	 * Attach event processor for any DOM element
	 * 
	 * @param {String}   evt     - event name
	 * @param {Function} handler - event handler
	 */
	on : function( evt, handler) {
		C.$populate();
		if (Jsl.isDefined( this.addEventListener))
		    this.addEventListener( evt, handler, false)
		; else if (Jsl.isDefined( this.attachEvent))
			this.attachEvent( 'on' + evt, handler)
		; else
			throw new Error( 'Jsl.Event Error: Incompatible browser.')
		;
	},

	/**
	 * Detach event processor for any DOM element
	 * 
	 * @param {String}   evt     - event name
	 * @param {Function} handler - event handler
	 */
	un : function( evt, handler) {
		C.$populate();
		if (Jsl.isDefined( this.removeEventListener))
			this.removeEventListener( evt, handler, false)
		; else if (Jsl.isDefined( this.detachEvent))
			this.detachEvent( 'on' + evt, handler)
		; else
			throw new Error( 'Jsl.Event Error: Incompatible browser.')
		;
	},

	/**
	 * Finds and returns an element(s) in DOM tree in current element's branch
	 * 
	 *  @param  {String} selector - CSS selector
	 *  @return {HTMLElement|Jsl.Collection}
	 */
	find : function( selector) {
		C.$populate( 'new');

		// no selector given return myself
		if (!selector) return this;

		// DOM element
		if (Jsl.isElement( selector)) {
			return selector;
		}

		// Search DOM elements by a given selector
		var els = Sizzle( selector, this) || [];

		// return the result
		return (els.length == 1) ? els[0] : (new C( els));
	},

	/**
	 * DOM manipulation
	 */

	/**
	 * Creates and returns created element, appends it to current element if possible
	 * 
	 * @param  {String|HTMLElement} - tag name or existing DOM element
	 * @return {HTMLElement}        - this element
	 */
	append : function( el) {
		C.$populate( 'new');
		if (!Jsl.isElement( el)) {
			try { el = d.createElement( el); } catch (e) { return null; }
		}
		this.appendChild && this.appendChild( el);
		return el;
	},

	/**
	 * Appends current element to a given element
	 * 
	 * @param  {HTMLElement}
	 * @return {HTMLElement} this
	 */
	appendTo : function( el) {
		C.$populate();
		return el.append( this);
	},

	/**
	 * Inserts a given element before current element
	 * 
	 * @param  {HTMLElement} - element to adjust before current element
	 * @return {HTMLElement} this
	 */
	before : function( el) {
		C.$populate();
		this.parentNode.insertBefore( el, this);
		return this;
	},

	/**
	 * Inserts a given element after a current element
	 * 
	 * @param  {HTMLElement} - element to adjust after current element
	 * @return {HTMLElement} this
	 */
	after : function( el) {
		C.$populate();
		this.parentNode.insertBefore( el, this.nextSibling);
		return this;
	},

	/**
	 * Creates and returns a copy of current element
	 * If deep parameter set as true - will create a copy of child nodes as well
	 * 
	 * @param  {Boolean}     - [Optional]
	 * @return {HTMLElement} - copy of current element
	 */
	clone : function( deep) {
		C.$populate( 'new');
		return this.cloneNode( deep);
	},

	/**
	 * Applies or returns CSS rules defined for current element
	 * If config is not set will get styles, otherwise will set styles
	 * 
	 * @param  {Object} config        - [Optional] css rules definition
	 * @return {CSSStyle|HTMLElement}
	 */
	css : function( config) {
		C.$populate();
		if (Jsl.isObject( config)) {
			Jsl.apply( this.style, config);
			return this;
		} else {
			return this.style;
		}
	},

	/**
	 * Removes all children from a current element
	 * 
	 * @return {HTMLElement} this
	 */
	empty  : function() {
		C.$populate();
		var children = this.childNodes, len = children.length, i = 0;
		for (; i < len; i++) {
			children[i].remove();
		}
		return this;
	},

	/**
	 * Removes current node from the DOM tree
	 */
	remove : _removeElement,

	/**
	 * Replaces a current node with a given node and returns a new node
	 * 
	 * @param  {HTMLElement} el
	 * @return {HTMLElement} el
	 */
	replace : function( el) {
		C.$populate( 'new');
		var
			self = this,
			p = this.parentNode
		;
		
		Jsl.freeElement( self);
		p.replaceChild( el, self);
		
		self = null;
		
		return el;
	},

	/**
	 * Attributes manipulation
	 */

	/**
	 * Adds a class tocurrent element className property
	 * 
	 * @param  {String} cls - class name(s)
	 * @return {HTMLElement} this
	 */
	addClass : function( cls) {
		C.$populate();
		if (!this.className) {
			this.className = cls;
		} else {
			var classes = (cls || '').split( /\s+/),
				cname   = ' ' + this.className.trim() + ' ', c
			;
			for (var i = 0, s = classes.length; i < s; i++) {
				c = classes[i] + ' ';
				if (cname.indexOf( ' ' + c) < 0) {
					cname += c;
				}
			}
			this.className = cname.trim();
		}
		return this;
	},

	/**
	 * Removes class(es) from curent element className property
	 * 
	 *  @param  {String} cls - class name(s)
	 *  @return {HTMLElement} this
	 */
	removeClass : function( cls) {
		C.$populate();
		if (!cls) {
			this.className = '';
		} else {
			var classes = (cls || '').split( /\s+/),
				cname   = (' ' + this.className.replace( /\s+/g, ' ') + ' '), c
			;
			for (var i = 0, s = classes.length; i < s; i++) {
				cname = cname.replace( ' ' + classes[i] + ' ', ' ');
			}
			this.className = cname.trim();
		}
		return this;
	},

	/**
	 * Returns or sets an atrribute from/to current element
	 * 
	 * If passed name and value - it will set the attribute and returns self element,
	 * otherwise, if only name argument given - will return a value of an attribute
	 * with a given name
	 * 
	 * @param  {String} name   - element's attribute name
	 * @param  {String} value  - [Optional] element's attribute value
	 * @return {HTMLElement|string}
	 */
	attr : function( name, value) {
		C.$populate();
		if (Jsl.isDefined( name) && Jsl.isDefined( value)) {
			this.setAttribute( name, value);
			return this;
		} else if (name) {
			return this.getAttribute( name);
		}
		return this;
	},

	/**
	 * Checks if given class(es) assigned to className property of current element
	 * 
	 * @param  {String} - class name(s)
	 * @return {Boolean}
	 */
	hasClass : function( cls) {
		var cname = ' ' + cls + ' ';
		if ((' ' + this.className + ' ').replace( /\s+/g, ' ').indexOf( cname) > -1) {
			return true;
		}
		return false;
	},

	/**
	 * Sets a given HTML string to innerHTML property of an element,
	 * or returns an innerHTML property value if no arguments passed
	 * 
	 * @param  {String} html - [Optional] - HTML string to set
	 * @return {String|HTMLElement}
	 */
	html : function( html) {
		C.$populate();
		if (!Jsl.isDefined( html) || Jsl.isNull( html)) {
			return this.innerHTML;
		}
		this.innerHTML = html;
		return this;
	},

	/**
	 * Toggles add/remove class(es) in className property of current element
	 * 
	 * @param  {String} cls  - class name(s)
	 * @param  {String} cls2 - class for replace cls or vise versa [optional]
	 * @return {HTMLElement} this
	 */
	toggleClass : function( cls, cls2) {
		C.$populate();
		if (cls2) {
			this.hasClass( cls) ?
				this.removeClass( cls).addClass( cls2) :
				this.removeClass( cls2).addClass( cls)
			;
		} else {
			this.hasClass( cls) ?
				this.removeClass( cls) : 
				this.addClass( cls)
			;
		}
		return this;
	},

	/**
	 * Traversing
	 */

	/**
	 * Returns all parents of current node as collection of HTMLElements
	 * 
	 * @return {Jsl.Collection} - collection of parent nodes
	 */
	parents : function() {
		C.$populate( 'new');
		var parents = [],
			parent = this
		;
		while (parent = parent.parentNode)
			parents[parents.length] = parent
		;
		return new C( parents);
	},

	/**
	 * Returns a text content of a node
	 */
	text : function() {
		if (Jsl.isIe) {
			return this.innerText;
		}
		return this.textContent;
	},

	/**
	 * Returns a first child of curent node in a DOM tree
	 * 
	 * @return {HTMLElement}
	 */
	first : function() {
		return this.childNodes[0] || null;
	},

	/**
	 * Iterating the elements of collection calls a user-defined function
	 * on each iteration
	 * 
	 * User-function will be called with 2 arguments:
	 *  @param {HTMLElement} - element
	 *  @param {Integer}     - element index inside collection
	 *  
	 * 
	 * @param {Finction} fn  - function to call
	 * @param {Object} scope - scope to bind a function
	 */
	each : function( fn, scope) {
		var el    = this,
			scope = scope || el
		;
		Jsl.each( el, fn, scope);
		return this;
	},

	/**
	 * Returns the last child of current node in DOM tree
	 * 
	 * @return {HTMLElement}
	 */
	last : function() {
		var cn = this.childNodes, len = cn.length;
		return (len > 0) ? cn[len - 1] : null;
	},

	/**
	 * Represents an element as an array containing this element
	 * This method is good to use whenever you won't think if the search returns an element or
	 * collection. In this case just call .toArray() will return always an array with at least one element inside
	 * 
	 * @return {Array}
	 */
	toArray : function() {
		return [this];
	}
};}());

var Hp = HTMLElement.prototype;

/**
 * Extending Collection class with populated properties of
 * HTMLElement prototype (looks like reverse-inheritence)
 * 
 * First of all - find all methods which must be populated
 */
Jsl.extendDomCollection = function() {
	try {
		if (Jsl.isIe && Element) {
			Jsl.apply( Element.prototype, HTMLElement.prototype);
		}
	} catch(err) {}

	var Hp = HTMLElement.prototype,
		populatedMethods = {}
	;

	for (var name in Hp) {
		if (!{}.hasOwnProperty || Hp.hasOwnProperty( name)) {
			try {
				var fn = Hp[name];
				if (Jsl.isFunction( fn)) {
					var m = fn.toString().match( /\$(populate)\s*\((.*?)\)/);
					if (m && m[1]) {
						var props = m[2].replace( /['\"\s]/g, '');
						populatedMethods[name] = new Function(
							"var a = arguments, r = this, n = '" + name + "';" +
							"var f = HTMLElement.prototype[n];" +
							"var p = '" + props + "'.split( ',');" +
							"if (p.inArray( 'new')) {" +
							"	r = new Jsl.Collection();" +
							"} else if (p.inArray( 'return')) {" +
							"	return f.apply( this, a);" +
							"}" +
							"this.each( function( el) {" +
							"	var o  = f.apply( el, a);" +
							"	if (Jsl.isElement( o)) {" +
							"		r.add( o);" +
							"	} else if (o instanceof Jsl.Collection) {" +
							"		r.merge( o);" +
							"	}" +
							"});" +
							"return r;"
						);
					}
				}
			} catch (err) {}
		}
	}
	/**
	 * Populate them!
	 */
	Jsl.applyIf( C.prototype, populatedMethods);
};

Jsl.extendDomCollection();

/**
 * An interface to define new dom methods!
 */
Jsl.extendDom = function( methods) {
	Jsl.apply( HTMLElement.prototype, methods);
	Jsl.extendDomCollection();
};

/**
 * Initializing window object event handlers
 */
Jsl.applyIf( w, {
	on : Hp.on,
	un : Hp.un
});

/**
 * Document extensions
 */
Jsl.applyIf( d, {
	/**
	 * Main selector function
	 * 
	 * Do you like $('#foo').setClass( 'bar'); syntax?
	 * Just create an appropriate binding:
	 * <code>
	 * var $ = document.query.bind( document);
	 * </code>
	 * You're on the way!
	 */
	query: function( selector) {
		// no selector given return myself
		if (!selector) return this;

		// if body element requested - returns it
		if (selector == 'body') {
			return this.body;
		}

		// check for standalone tag
		var m = selector.match( /^<(\w+)\s*\/?>(?:<\/\1>)?$/);
		if (m && m[1]) {
			// create and return an element
			return d.createElement( m[1]);
		}

		// return the result
		return Hp.find.call( this, selector);
	}
});

}());
